<?php

class Yz_Item_List
    extends Yz_Class
{

    protected $itemClass;
    protected $model;
    protected $all; // number of items in source table

    /**
     *   @param object|string class
     */
    public function __construct( $model )
    {
        $this->model = $model;
        $this->itemClass = $model->getItemClass();
    } // __construct



    /** toString()
     *
     *   @return string
     */
    public function __toString()
    {
        $cl = strtr($this->itemClass, '\\_', '..');
        $count = $this->count();
        return "List<{$cl}>[{$count}]";
    }

    /** Whether the list entry exists and is the same item as $item
     *      This fn is almost the same as _has_ but without additional index checks
     *      and compares both the index and the items in question
     *  @return bool
     */
    public function contains(Yz_Item_Interface $item)
    {
        $id = $item->getId();
        if (! $id)
            return false;
        return isset($this->__dict__[$id]) and $this->__dict__[$id] === $item;
    }


    /** Load all items (if not already loaded)
     *      NOTE: the difference between all() and all other list fetch methods is that
     *      the result of all() is always equal to all items of this list, unless the latter
     *      contains new items which have not been saved yet (but those are probably also
     *      expected to be on the list)
     *  @return this
     */
    public function all()
    {
        if (! $this->all) { // do not load all items again
            $result = $this->fetchList();
            assert('$result instanceof Yz_Action_SelectList');
            // the result list is ignored
            $this->assignList($result);
            $this->all = count($this->__dict__);
            // TODO also forward this to parent model's list
        }
        return $this;
    }

    /**
     *  @return Yz_List{item}
     */
    public function page(Yz_Pagination_Interface $p8n)
    {
        assert('! $this->all');
        $result = $this->fetchList($p8n->getPageOffset(), $p8n->getPageItems());
        assert('$result instanceof Yz_Action_SelectList');
        $list = $this->assignList($result);
        $this->all = $result->getFoundRows();
        $p8n->setItemCount($this->all);
            // TODO also forward this to parent model's list
        return $list;
    }


    /** Add item to list after value assignment (not "add to storage")
     *      This fn is apparently the same as _set_ but without additional index checks
     *  @return this
     *  @throw ValueError if item has no ID
     *  @throw StateError if item's ID is already on the list
     */
    public function add(Yz_Item_Interface $item)
    {
        if (! ($item instanceof $this->itemClass))
            throw TypeError::_new_();
        $my =& $this->__dict__;
        $index = $item->getId();
        if (! $index)
            throw ValueError::_new_();
        if (isset($my[$index])) {
            // item exists, it must be the same object
            if ($my[$index] !== $item)
                throw ValueError::_new_();
        } else {
            $my[$index] = $item;
        }
        return $this;
    }

    /** Remove item from list after its deletion (not "delete from storage")
     *      This fn is apparently the same as _del_ but without additional index checks
     *  @return this
     *  @throw ValueError if item has no ID
     *  @throw StateError if item's ID is already on the list
     */
    public function remove(Yz_Item_Interface $item)
    {
        if (! ($item instanceof $this->itemClass))
            throw TypeError::_new_();
        $my =& $this->__dict__;
        $index = $item->getId();
        if (! $index)
            throw ValueError::_new_();
        assert('isset($my['.$index.']) and $my['.$index.'] === $item');
        unset($my[$index]);

        $parentModel = $this->model;
        while ($parentModel = $parentModel->getParentModel())
            unset($parentModel->getItems()->__dict__[$index]);
        return $this;
    }



    /** validate the index, adjust it if required/possible
     *   @param mixed index
     *   @return mixed valid index
     *   @throw TypeError if index not valid
     *   @throw ValueError if index not valid
     */
    protected function adjustIndex( $index )
    {
        if (! is_numeric($index))
            throw TypeError::_new_();
        if ($index <= 0)
            throw ValueError::_new_();
        return (int)$index;
    }


    /** get item from the list or try to fetch from DB
     *  @param string index
     *  @return Item
     *  @throw IndexError
     */
    protected function _get_( $index )
    {
        $my =& $this->__dict__;
        if (isset($my[$index])) {
            $item = $my[$index];
        } else {
            try {
                $item = $this->fetchItem($index);
                #$my[$index] = $item;
            } catch (NotFoundError $ex) {
                throw IndexError::_new_(array('index' => $index));
            }
        }
        return $item;
    }


    /** Internally insert item into the list
     *  @param string index
     *  @param Item value
     *  @return this
     *  @throw TypeError
     *  @throw ValueError if value's id does not match index,
     *      or if an item with this id already exists in list and is not the same object
     */
    protected function _set_( $index, $value )
    {
        if (! ($value instanceof $this->itemClass))
            throw TypeError::_new_();
        if ($value->getId() !== $index)
            throw ValueError::_new_();
        $my =& $this->__dict__;
        if (isset($my[$index])) {
            if ($my[$index] !== $value)
                throw ValueError::_new_();
        } else {
            $my[$index] = $value;
        }

        return $this;
    }

    /** Unset (delete) item from list and database
     *  @param string index
     *  @return this
     *  @throw IndexError
     */
    protected function _del_( $index )
    {
        $my =& $this->__dict__;
        if (! isset($my[$index]))
            throw IndexError::_new_(array('index' => $index));

        // delete item: this also calls this.remove and forwards up parent models
        $my[$index]->delete();
        #unset($my[$index]);
        return $this;
    }


    /** Assign result values to items and return an array of the result items
     *  @return array
     */
    protected function assignList(Yz_Action_Interface $result)
    {
        debug();
        $list = array();
        $my =& $this->__dict__;
        while ($ent = $result->fetch()) {
            $id = (int)$ent->id;
            if (isset($my[$id])) // do NOT reassign values for already loaded items
                $item = $my[$id];
            else
                $item = $this->model->item()->assignValues($ent);
            $list[$id] = $item;
        }
        return new Yz_List($list, $this->itemClass);
    }

    /** Fetch item in case it is not present on the list
     *  @return Item
     *  @throw NotFoundError
     */
    protected function fetchItem($id)
    {
        $item = $this->model->item()->setId($id);
        return $item;
        //////////////////////////////
        $result = $this->model
            ->selectAction('SelectById')
            ->setId($id)
            ->fetch()
        ;
        $item = $this->model->item()->assignValues($result);
        return $item;
    }


    /** fetch item list from the storage source
     *  @return Yz_Action
     */
    protected function fetchList($offset=null, $limit=null)
    {
        debug();
        $action = $this->model->selectAction('SelectList');
        if (null !== $offset and null !== $limit)
            $action
                ->setOffset($offset)
                ->setLimit($limit)
            ;
        return $action->perform() ;
    }

}
